# coding=latin-1
''' Contests are sets of matches between many agents. Its purpose its to 
    evaluate each agents in comparison with the others.
    Each contests its arranged in a different way.
    
    @author: Leonardo Val <lval@ucu.edu.uy>
'''

import itertools, random, collections
from _base import match
from _utils import randgen
from five_field_kono import Five_field_kono

class Stats(): #################################################################
    ''' Statistics accumulator for Contest classes.
    '''
    def __init__(self):
        lambda0 = lambda: 0
        self._stats = collections.OrderedDict([
            ('keys', {}),
            ('matches_played', collections.defaultdict(lambda0)),
            ('matches_won', collections.defaultdict(lambda0)),
            ('matches_lost', collections.defaultdict(lambda0)),
            ('result_sum', collections.defaultdict(lambda0)),
            ('result_sum2', collections.defaultdict(lambda0))
        ])
        self.__dict__.update(self._stats)
    
    def clear(self):
        ''' Clears all statistics.
        '''
        for stat in self._stats.itervalues():
            stat.clear()
    
    def inc(self, stat, key):
        return self.add(stat, key, 1)
    
    def add(self, stat, key, value):
        new_value = stat[key] + value
        stat[key] = new_value
        return new_value
    
    def process(self, agents, match_num, move_num, d, game):
        ''' Accumulates statistics for each tuple generated by the contest.
        '''
        if move_num is None: # Finished match.
            results = d
            for player, agent in agents.iteritems():
                self.keys.setdefault(agent, agent.name)
                self.keys.setdefault(player, str(player))
                self.inc(self.matches_played, agent)
                self.inc(self.matches_played, player)
                result = results[player]
                if result: # Nonzero result means match is not a draw.
                    if result > 0: # Victory.
                        self.inc(self.matches_won, agent)
                        self.inc(self.matches_won, player) 
                    if result < 0: # Defeat.
                        self.inc(self.matches_lost, agent)
                        self.inc(self.matches_lost, player)
                    self.add(self.result_sum, agent, result)
                    self.add(self.result_sum2, agent, result ** 2)
                    self.add(self.result_sum, player, result)
                    self.add(self.result_sum2, player, result ** 2)
    
    def __str__(self):
        ''' Prints the statistics gathered in tabular form.
        '''
        keys = self.keys.keys()
        keys.sort(lambda a1, a2: cmp(a1, a2))
        return ','.join(self._stats.iterkeys()) +'\n'+ '\n'.join(
            [','.join([str(stat[key]) for stat in self._stats.itervalues()]) for key in keys])

class Contest(object): #########################################################
    ''' Base class for all contests. Defines a common statistics gathering and
        contest's matches handling.
    '''
    
    def __init__(self, game, agents, stats=None):
        self.game = game
        self.agents = list(agents)
        self.stats = Stats() if stats is None else stats
        
    def run(self, matches):
        ''' Receives a list of matches, given as tuples (game, agents) where
            agent {player:agent}. This method run each of the matches, returning
            every step of each match.
            The last item returned is a tuple (None, None, stats, game) where
            stats is a dict with the statistics gathered for each agent.
        '''
        self.stats.clear() # Erases previous statistics.
        for match_num, (game, agents) in zip(xrange(10**5), matches):
            for move_num, d, g  in match(game, **agents):
                self.stats.process(agents, match_num, move_num, d, g)
                yield (match_num, move_num, d, g)
        yield (None, None, self.stats, self.game)
    
    def log(self, matches=None):
        ''' Transforms a contest generator into a line generator, that
            can be used to display in the screen or write in a file.
        '''
        for n1, n2, a, _ in self.run(matches):
            if n1 is None:
                yield 'Agent,'+ ','.join([n for n in a.iterkeys()])
                for agent in self.agents:
                    yield agent.name +','+ ','.join([str(stat[agent]) for stat in a.itervalues()])
            elif n2 == 0 or n2 is None:
                yield '[%d]: %s' % (n1, ', '.join(['%s:%s' % i for i in a.iteritems()]))
            else:
                yield '[%d] #%d %s' % (n1, n2, a)  

def complete(contest):
    for match_num, _, d, _ in contest.run():
        if match_num is None: # Contest is over.
            return d
        
class AllAgainstAll_Contest(Contest): ##########################################
    ''' All agents play count matches against all other agents, in all possible
        combinations. 
    '''
    def __init__(self, game, agents, count=1):
        Contest.__init__(self, game, agents)
        self.count = count
        
    def run(self):
        players = self.game.players
        arrays = itertools.permutations(self.agents, len(players))
        matches = [(self.game, dict(zip(players, array))) for array in arrays for _ in xrange(self.count)]
        return Contest.run(self, matches)

class Sampling_Contest(Contest): ###############################################
    ''' Built so each agent will play a match with upto count randomly selected
        opponents. Be warned that depending on the agents number and count, 
        some agents may play less matches that count.
    '''
    def __init__(self, game, agents, random=None, count=1):
        Contest.__init__(self, game, agents)
        self.random = randgen(random)
        self.count = count
    
    def matches(self):
        control = dict([(agent, self.count) for agent in self.agents])
        players = self.game.players
        player_count = len(players)
        while len(control) > player_count:
            agents = control.keys()
            self.random.shuffle(agents)
            for array in itertools.cycle(itertools.permutations(agents, player_count)):
                yield (self.game, dict(zip(players, array)))
                remove_agents = False
                for agent in array:
                    agent_count = control.pop(agent) - 1
                    if agent_count > 0:
                        control[agent] = agent_count 
                    else:
                        remove_agents = True
                if remove_agents:
                    break
        
    def run(self):
        return Contest.run(self, self.matches())

class Sort_Contest(Contest): ###################################################
    ''' Agents are sorted using count matches between them and comparing the
        results. Which matches and how many times each agent plays depends on
        the sort algorithm. Shuffling the agents list is recommended.
        This is only usable with 2 player games.
    '''
    def __init__(self, game, agents, count=1):
        Contest.__init__(self, game, agents)
        self.count = count
        self.__matches__ = []
        
    def comp_fun(self, agent1, agent2):
        players = self.game.players
        agents = dict(zip(players, [agent1, agent2]))
        results_agent1 = []
        results_agent2 = []
        for _ in xrange(self.count):
            m = list(match(self.game, **agents))
            self.__matches__.append((agents, m))
            _, results, _ = m[-1]
            results_agent1.append(results[players[0]])
            results_agent2.append(results[players[1]])
        return sum(results_agent1) - sum(results_agent2)
    
    def run(self):
        self.__matches__ = []
        self.agents.sort(self.comp_fun)
        self.stats.clear() # Erases previous statistics.
        for match_num, (agents, moves) in zip(xrange(len(self.__matches__)), self.__matches__):
            for move_num, d, g in moves:
                self.stats.process(agents, match_num, move_num, d, g)
                yield (match_num, move_num, d, g)
        yield (None, None, self.stats, self.game)

class Pyramid_Contest(Contest): ################################################
    ''' Agents play count matches againts other. The winner gets to the next 
        round, and so on until the contest has one winner.
    '''
    def __init__(self, game, agents, count=1):
        Contest.__init__(self, game, agents)
        self.count = count
        
    def run(self):
         ''' TODO
        self.__matches__ = []
        players = self.game.players
        winners = list(self.agents)
        iter_agents = itertools.cycle(winners)
        matches = [[iter_agents.next() for _ in xrange(len(players))] for _ in xrange(round(len(winners) / 2))]
        '''

if __name__ == '__main__': #####################################################
    from tictactoe import TicTacToe
    from _agents import RandomAgent, MiniMaxAgent, AlphaBetaAgent
    from heuristicas import *
    rnd = random.Random()
    heuristicaSinGenotipo = heuristic_wrap()
    agentes = [AlphaBetaAgent('MiniMaxAgent_%05d' % i, 3, rnd,heuristic=heuristicaSinGenotipo.heuristicaDistanciaDestino)for i in xrange(1)]
    #agentes.extend([RandomAgent(rnd, 'RandomAgent_%05d' % i)  for i in xrange(1)])
    agentes.extend([AlphaBetaAgent('MiniMaxAgent_%05d' % i, 3, rnd,heuristic=heuristicaSinGenotipo.heuristicaDistanciaDestino) for i in xrange(1)])
    print complete(AllAgainstAll_Contest(Five_field_kono(), agentes, 1))
